use std::collections::HashMap;

use anyhow::Result;
use bollard::{
    container::{
        self, CreateContainerOptions, ListContainersOptions, RemoveContainerOptions,
        StartContainerOptions,
    },
    exec::{CreateExecOptions, StartExecOptions},
    secret::{
        ContainerCreateResponse, ContainerSummary, DeviceRequest, HostConfig, PortBinding, PortMap,
        RestartPolicy, RestartPolicyNameEnum,
    },
    Docker,
};
use futures_util::StreamExt;

use crate::Dep;

pub async fn is_container_exist(docker: &Docker, name: &str) -> Result<Option<ContainerSummary>> {
    let res = docker
        .list_containers(Some(ListContainersOptions {
            all: true,
            filters: HashMap::from([("name", vec![name])]),
            ..Default::default()
        }))
        .await?;

    if res.is_empty() {
        return Ok(None);
    }

    let container = res.into_iter().find(|c| {
        if let Some(names) = &c.names {
            names
                .iter()
                .any(|n| n.strip_prefix('/').is_some_and(|n| n == name))
        } else {
            false
        }
    });

    Ok(container)
}

pub async fn create_container(
    docker: &Docker,
    dep: Dep,
    curr_path: &str,
    network: Option<String>,
) -> Result<ContainerCreateResponse> {
    let binds = dep.volumes.map(|volumes| {
        volumes
            .into_iter()
            .map(|vol| format!("{}/{}", curr_path, vol))
            .collect()
    });

    let (port_bindings, exposed_ports) = if let Some(ports) = dep.ports {
        let mut bindings: PortMap = HashMap::new();
        let mut exposed: HashMap<String, HashMap<(), ()>> = HashMap::new();
        for (dst, src) in ports {
            bindings.insert(
                src.clone(),
                Some(vec![PortBinding {
                    host_port: Some(dst),
                    ..Default::default()
                }]),
            );
            exposed.insert(src, HashMap::<(), ()>::new());
        }
        (Some(bindings), Some(exposed))
    } else {
        (None, None)
    };

    let device_requests = dep.devices.map(|devices| {
        devices
            .iter()
            .filter_map(|device| serde_json::from_str::<DeviceRequest>(device).ok())
            .collect()
    });

    let host_config = dep.network.or(network).map(|network| HostConfig {
        network_mode: Some(network),
        binds,
        port_bindings,
        restart_policy: Some(RestartPolicy {
            name: Some(RestartPolicyNameEnum::ON_FAILURE),
            maximum_retry_count: Some(3),
        }),
        device_requests,
        ..Default::default()
    });

    let config = container::Config {
        image: Some(dep.image),
        env: dep.envs,
        cmd: dep.cmd,
        host_config,
        exposed_ports,
        ..Default::default()
    };

    let res = docker
        .create_container(
            Some(CreateContainerOptions {
                name: dep.name.clone(),
                platform: None,
            }),
            config,
        )
        .await?;

    Ok(res)
}

pub async fn start_container(docker: &Docker, name: &str) -> Result<()> {
    docker
        .start_container(name, None::<StartContainerOptions<String>>)
        .await?;
    Ok(())
}

pub async fn remove_container(docker: &Docker, name: &str) -> Result<()> {
    docker
        .remove_container(
            name,
            Some(RemoveContainerOptions {
                force: true,
                ..Default::default()
            }),
        )
        .await?;
    Ok(())
}

pub async fn exec_container(docker: &Docker, container_name: &str, cmd: Vec<String>) -> Result<()> {
    let res = docker
        .create_exec(
            container_name,
            CreateExecOptions {
                cmd: Some(cmd),
                attach_stdout: Some(true),
                attach_stderr: Some(true),
                ..Default::default()
            },
        )
        .await?;

    let res = docker.start_exec(&res.id, None::<StartExecOptions>).await?;
    if let bollard::exec::StartExecResults::Attached {
        mut output,
        input: _,
    } = res
    {
        while let Some(Ok(msg)) = output.next().await {
            match msg {
                container::LogOutput::StdOut { message }
                | container::LogOutput::StdErr { message } => {
                    println!("{}", String::from_utf8_lossy(&message));
                }
                _ => {}
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod container_tests {
    use std::env;

    use bollard::API_DEFAULT_VERSION;

    use super::*;

    fn new_docker() -> Docker {
        // Docker::connect_with_unix_defaults().unwrap()
        Docker::connect_with_unix("/tmp/podman.sock", 30, API_DEFAULT_VERSION).unwrap()
    }

    #[tokio::test]
    async fn test_is_container_exist() {
        let docker = new_docker();
        let container = is_container_exist(&docker, "pg_test").await.unwrap();
        println!("pg_test exist: {:?}", container);
    }

    #[tokio::test]
    async fn test_create_container() {
        let curr_path = env::current_dir().unwrap();
        let curr_path = curr_path.to_str().unwrap_or(".");
        let docker = new_docker();

        create_container(
            &docker,
            Dep {
                name: "pg_test".into(),
                image: "postgres:17.2".into(),
                network: None,
                envs: Some(vec!["POSTGRES_PASSWORD=123456".into()]),
                volumes: Some(vec!["target/data:/var/lib/postgresql/data".into()]),
                ports: Some(vec![("25432".into(), "5432/tcp".into())]),
                cmd: None,
                after_cmd: None,
                devices: None,
            },
            curr_path,
            None,
        )
        .await
        .unwrap();
    }

    #[tokio::test]
    async fn test_start_container() {
        let docker = new_docker();
        start_container(&docker, "pg_test").await.unwrap();
    }

    #[tokio::test]
    async fn test_remove_container() {
        let docker = new_docker();
        remove_container(&docker, "pg_test").await.unwrap();
    }

    #[tokio::test]
    async fn test_exec_container() {
        let docker = new_docker();

        exec_container(
            &docker,
            "pg_test",
            vec![
                "psql".into(),
                "-U".into(),
                "postgres".into(),
                "-c".into(),
                "create database mytestdb;".into(),
            ],
        )
        .await
        .unwrap();
    }
}
